SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
6 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
13.4 KB · 249 lines tsx
Raw Blame History
1import type { Metadata } from "next";2import Link from "next/link";3import { notFound } from "next/navigation";4import { Breadcrumbs } from "@/components/entity/breadcrumbs";5import { EntityMap } from "@/components/entity/entity-map";6import { JsonLd } from "@/components/entity/json-ld";7import { ExtLink, KeyValueList } from "@/components/entity/kv";8import { SectionNav } from "@/components/entity/section-nav";9import { SourcesFooter } from "@/components/entity/sources-footer";10import { FacilityTable } from "@/components/facility-table";11import { ApiNote, HonestEmpty, ShareList } from "@/components/intelligence/shared";12import { StatusBadge } from "@/components/ui/badge";13import { Page, Section } from "@/components/ui/card";14import { CountryFlag } from "@/components/ui/country-flag";15import { Metric, MetricCell, MetricRow } from "@/components/ui/metric";16import { OfflineNotice } from "@/components/ui/states";17import { RowLink, TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";18import { SourceHistory } from "@/components/ui/timeline";19import { api } from "@/lib/api";20import { countryName, fmtInt, placeLine, pluralize } from "@/lib/format";21import { routes } from "@/lib/routes";22import { NOT_FOUND_METADATA, absoluteUrl, compactJsonLd, pageMetadata } from "@/lib/seo";23import { countryHrefMap } from "../../cloud-regions/cloud-format";2425export const revalidate = 300;2627type Params = Promise<{ slug: string }>;2829function describe(d: { name: string; nameLong: string | null; city: string | null; countryIso2: string | null; networkCount: number | null; facilityCount: number; operators?: unknown[] }): string {30  const place = [d.city, d.countryIso2 ? countryName(d.countryIso2) : null].filter(Boolean).join(", ");31  const parts = [`${d.name}${d.nameLong && d.nameLong !== d.name ? ` (${d.nameLong})` : ""} — internet exchange point${place ? ` in ${place}` : ""}`];32  if (d.networkCount !== null) parts.push(`${fmtInt(d.networkCount)} connected networks`);33  parts.push(`present in ${fmtInt(d.facilityCount)} indexed ${d.facilityCount === 1 ? "facility" : "facilities"}`);34  if (d.operators?.length) parts.push(`${fmtInt(d.operators.length)} operators`);35  return `${parts.join(", ")}. Facilities, operators, nearby infrastructure and sources.`;36}3738/** External identifier → public URL when the registry is known. */39function idHref(key: string, value: string | number): string | null {40  const k = key.toLowerCase();41  if (k.includes("peeringdb")) return `https://www.peeringdb.com/ix/${value}`;42  if (k === "wikidata") return `https://www.wikidata.org/wiki/${value}`;43  if (typeof value === "string" && /^https?:\/\//.test(value)) return value;44  return null;45}4647export async function generateMetadata({ params }: { params: Params }): Promise<Metadata> {48  const { slug } = await params;49  const r = await api.ixp(slug);50  if (!r.ok) return r.status === 404 ? NOT_FOUND_METADATA : { title: "Internet exchange" };51  return pageMetadata({ title: r.data.name, description: describe(r.data), path: routes.ixp(r.data.slug) });52}5354export default async function IxpPage({ params }: { params: Params }) {55  const { slug } = await params;56  const [r, countryHref] = await Promise.all([api.ixp(slug), countryHrefMap()]);57  if (!r.ok && r.status === 404) notFound();58  if (!r.ok) {59    return (60      <Page className="py-6">61        <Breadcrumbs items={[{ name: "Internet exchanges", href: routes.ixps() }, { name: slug, href: routes.ixp(slug) }]} className="mb-4" />62        <OfflineNotice detail={r.error} />63      </Page>64    );65  }66  const d = r.data;67  const cName = d.countryName ?? (d.countryIso2 ? countryName(d.countryIso2) : null);68  const cHref = countryHref(d.countryIso2);69  const ids = Object.entries(d.externalIds ?? {});70  const facilities = d.facilities ?? [];71  const operators = [...(d.operators ?? [])].sort((a, b) => b.facilityCount - a.facilityCount);72  const opTotal = operators.reduce((a, o) => a + o.facilityCount, 0) || 1;73  const nearby = d.nearbyFacilities ?? [];74  const hasGeo = typeof d.lat === "number" && typeof d.lng === "number";75  const history = d.sourceHistory ?? [];7677  const jsonLd = compactJsonLd({78    "@context": "https://schema.org",79    "@type": "Organization",80    name: d.name,81    alternateName: d.nameLong && d.nameLong !== d.name ? d.nameLong : undefined,82    url: absoluteUrl(routes.ixp(d.slug)),83    sameAs: [d.website, ...ids.map(([k, v]) => idHref(k, v))].filter((x): x is string => !!x),84    description: describe(d),85    address: d.city || cName ? compactJsonLd({ "@type": "PostalAddress", addressLocality: d.city, addressCountry: d.countryIso2 }) : undefined,86  });8788  const sections = [89    { id: "location", label: "Location" },90    { id: "facilities", label: "Facilities", count: facilities.length },91    { id: "operators", label: "Operators", count: operators.length },92    { id: "nearby", label: "Nearby infrastructure" },93    { id: "identifiers", label: "Identifiers", count: ids.length },94    { id: "history", label: "Source history", count: history.length },95  ];9697  return (98    <Page>99      <JsonLd data={jsonLd} />100      <Breadcrumbs items={[{ name: "Internet exchanges", href: routes.ixps() }, { name: d.name, href: routes.ixp(d.slug) }]} className="pt-4" />101102      <header className="py-4 sm:py-6">103        <div className="label-caps mb-1.5">Internet exchange</div>104        <h1 className="text-[22px] font-semibold leading-tight tracking-[-0.02em] text-ink sm:text-[28px]">{d.name}</h1>105        {d.nameLong && d.nameLong !== d.name && <p className="mt-1 text-[14px] text-ink-2">{d.nameLong}</p>}106        <p className="mt-2 flex flex-wrap items-center gap-x-1.5 text-[13px] text-ink-2">107          {d.countryIso2 && <CountryFlag iso2={d.countryIso2} size={13} />}108          {d.city && <span>{d.city}</span>}109          {cName && (110            <>111              {d.city && <span className="text-ink-4">·</span>}112              {cHref ? (113                <Link href={cHref} className="hover:text-ink hover:underline">114                  {cName}115                </Link>116              ) : (117                <span>{cName}</span>118              )}119            </>120          )}121          {d.metro && (122            <>123              <span className="text-ink-4">·</span>124              <Link href={routes.market(d.metro.slug)} className="hover:text-ink hover:underline">125                {d.metro.name} market126              </Link>127            </>128          )}129          {d.website && (130            <>131              <span className="text-ink-4">·</span>132              <ExtLink href={d.website} className="text-ink-2" />133            </>134          )}135        </p>136      </header>137138      <MetricRow cols={4}>139        <MetricCell>140          <Metric value={d.networkCount === null ? "—" : fmtInt(d.networkCount)} label="Participants (networks)" note={d.networkCount === null ? "not published by the sources indexed" : "as published by the source"} />141        </MetricCell>142        <MetricCell>143          <Metric value={fmtInt(d.facilityCount)} label="Facilities" note="indexed data centers with this exchange present" />144        </MetricCell>145        <MetricCell>146          <Metric value={fmtInt(operators.length)} label="Operators" note={operators.length ? "operating those facilities" : "no facility linked yet"} />147        </MetricCell>148        <MetricCell>149          <Metric value={d.regionContinent ?? cName ?? "—"} label={d.regionContinent ? "Region" : "Country"} />150        </MetricCell>151      </MetricRow>152153      <SectionNav items={sections} className="mt-4" />154155      <div className="mt-6 flex flex-col gap-8">156        <Section id="location" title="Location" description={hasGeo ? "Reference point of the exchange (metro reference when the exchange publishes no coordinates)." : "The exchange publishes no coordinates of its own."} className="scroll-mt-28">157          {hasGeo ? <EntityMap center={{ lat: d.lat as number, lng: d.lng as number }} zoom={9} filters={d.countryIso2 ? { country: d.countryIso2 } : undefined} precision="metro" openLabel={`Open ${d.name} in map`} className="h-[220px] sm:h-[320px]" /> : <HonestEmpty title="No coordinates" detail={d.metro ? <>the exchange is placed in the <Link href={routes.market(d.metro.slug)} className="underline decoration-hair-2 hover:text-ink">{d.metro.name}</Link> market on maps</> : "no metro reference either — the exchange is listed but not mapped"} />}158          <div className="mt-3">159            <KeyValueList160              items={[161                { label: "City", value: d.city },162                { label: "Country", value: cName ? (cHref ? <Link href={cHref} className="hover:text-accent hover:underline">{cName}</Link> : cName) : null },163                { label: "Market", value: d.metro ? <Link href={routes.market(d.metro.slug)} className="hover:text-accent hover:underline">{d.metro.name}</Link> : null },164                { label: "Website", value: d.website ? <ExtLink href={d.website} /> : null },165              ]}166              columns={2}167            />168          </div>169          <ApiNote note={d.trafficNote} label="Traffic & location" className="mt-3" />170        </Section>171172        <Section id="facilities" title="Facilities where this exchange is present" description={facilities.length ? `${fmtInt(facilities.length)} indexed ${pluralize(facilities.length, "facility", "facilities")}` : undefined} className="scroll-mt-28">173          {facilities.length ? <FacilityTable items={facilities} columns={["operator", "market", "country", "status", "mw"]} /> : <HonestEmpty title="No facility linked yet" detail="facility presence comes from the exchange's published facility list or from facility pages; it appears as soon as it is observed" />}174        </Section>175176        <Section id="operators" title="Operators present" description="Operators of the facilities where the exchange is reachable, by number of facilities." className="scroll-mt-28">177          {operators.length ? <ShareList items={operators.slice(0, 20).map((o) => ({ key: o.id, label: o.name, href: routes.operator(o.slug), count: o.facilityCount, share: o.facilityCount / opTotal }))} /> : <HonestEmpty title="No operator known" detail="operators appear once a linked facility carries one" />}178        </Section>179180        <Section id="nearby" title="Nearby infrastructure" description="Indexed facilities around the exchange's reference point, by distance." className="scroll-mt-28">181          {nearby.length ? (182            <Table dense mobileCollapse={{ total: nearby.length, label: "facilities" }}>183              <THead>184                <tr>185                  <Th>Facility</Th>186                  <Th className="hidden sm:table-cell">Operator</Th>187                  <Th className="hidden md:table-cell">Place</Th>188                  <Th>Status</Th>189                  <Th numeric>Distance</Th>190                </tr>191              </THead>192              <TBody>193                {nearby.map((f) => (194                  <Tr key={f.id}>195                    <Td primary>196                      <RowLink href={routes.facility(f.slug)}>{f.name}</RowLink>197                      <div className="text-[11.5px] text-ink-3 sm:hidden">{f.operator?.name ?? "Operator unknown"}</div>198                    </Td>199                    <Td label="Operator" className="hidden sm:table-cell">200                      {f.operator ? (201                        <Link href={routes.operator(f.operator.slug)} className="text-ink-2 hover:text-ink hover:underline">202                          {f.operator.name}203                        </Link>204                      ) : (205                        <span className="text-ink-4">—</span>206                      )}207                    </Td>208                    <Td label="Place" className="hidden text-ink-3 md:table-cell">209                      {placeLine(f)}210                    </Td>211                    <Td label="Status">212                      <StatusBadge status={f.status} short size="xs" />213                    </Td>214                    <Td label="Distance" numeric lead>215                      {f.distanceKm < 1 ? `${Math.round(f.distanceKm * 1000)} m` : `${f.distanceKm.toFixed(1)} km`}216                    </Td>217                  </Tr>218                ))}219              </TBody>220            </Table>221          ) : (222            <HonestEmpty title={hasGeo ? "No other indexed facility nearby" : "Not computed"} detail={hasGeo ? "within the search radius" : "the exchange has no coordinates, so distances cannot be measured"} />223          )}224        </Section>225226        <Section id="identifiers" title="Identifiers" description="External registry identifiers as observed." className="scroll-mt-28">227          {ids.length ? (228            <KeyValueList229              items={ids.map(([k, v]) => {230                const href = idHref(k, v);231                return { label: k.replace(/_/g, " "), value: href ? <ExtLink href={href}>{String(v)}</ExtLink> : <span className="figure text-[12.5px]">{String(v)}</span> };232              })}233              columns={2}234            />235          ) : (236            <HonestEmpty title="No external identifier recorded" />237          )}238        </Section>239240        <Section id="history" title="Source history" description="Observations that shaped this record." className="scroll-mt-28">241          {history.length ? <SourceHistory items={history.slice(0, 40)} /> : <HonestEmpty title="No source history yet" />}242        </Section>243      </div>244245      <SourcesFooter sources={r.sources} updatedAt={d.updatedAt} note="Exchange facts are shown as published by PeeringDB and the exchange operator; participant counts are never estimated and coordinates are metro reference points unless the exchange publishes its own." />246    </Page>247  );248}249